/* ************************************************************************ */
/*                                                                          */
/*  Copyright (c)2005 Russell Weir | damonsbane _at_ gmail                  */
/*                                                                          */
/* This library is free software; you can redistribute it and/or            */
/* modify it under the terms of the GNU Lesser General Public               */
/* License as published by the Free Software Foundation; either             */
/* version 3.0 of the License, or (at your option) any later version.       */
/*                                                                          */
/* This library is distributed in the hope that it will be useful,          */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of           */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        */
/* Lesser General Public License or the LICENSE file for more details.      */
/*                                                                          */
/* ************************************************************************ */

package altdate;
import altdate.AltDate.TimeStruct;

class JulianDay extends AltDate {
	public var value(default,default) : Float;

#if neko
	public static function main() {
		var args = neko.Sys.args();
		if(args.length == 0 || args[0] == "--help") {
			neko.Lib.println("Usage:");
			neko.Lib.println("Julian day #");
			neko.Lib.println("- or -");
			neko.Lib.println("year month day [hh:mm:ss] where month is 1-12");
			neko.Lib.println("1858 11 16 12:00:00 should be Julian Day 2400000");
			neko.Lib.println("2132 8 31 12:00:00 should be Julian Day 2500000");
			neko.Sys.exit(0);
		}
		if(args.length == 1) {
			var jd = new JulianDay(true);
			jd.value = Std.parseFloat(args.shift());
			var ts = convertToTimeStruct(jd.value);
			ts.month = ts.month +1;
			neko.Lib.print(ts.year+"-"+ts.month+"-"+ts.day +" ");
			neko.Lib.println(ts.hours+":"+ts.minutes+":"+ts.seconds);
			neko.Lib.println(convertToGregorianDateTime(jd));
			neko.Lib.println("The two values above should match, except formatting");
			neko.Sys.exit(0);
		}
		var y = Std.parseInt(args.shift());
		var m = Std.parseInt(args.shift()) - 1;
		var d = Std.parseInt(args.shift());
		var seconds : Int = 0;
		var ts : String = "00:00:00";
		if(args.length > 0) {
			ts = args.shift();
			seconds = Tools.timeToSeconds(ts);
		}
		var jd = new JulianDay(true);
		jd.value = calculate(y,m,d,seconds);
		neko.Lib.println(y + "-" + Tools.monthToString(m) + "-" + d + " " + ts + " = ");
		neko.Lib.println("\tJulian Day #" + jd);
		neko.Lib.println("\tWhich is a "+ Tools.dayToString(jd.getDay()));
	}
#end

	public function new(asDateTime : Bool, ?v : Null<Float>) {
		super();
		if(asDateTime == true)
			is_date_time = true;
		else
			is_date_time = false;
		if(v != null)
			value = v;
		else
			value = 0;
	}

	public function getValue() : Float {
		return value;
	}

	public function setValue(v : Float) : Float {
		value = v;
		return value;
	}

	/**
		Return day number in week, 0=Sunday, like Date.getDay()
	*/
	public function getDay() : Int {
		return Tools.int(value + 1.5) % 7;
	}

	public function toString() : String {
		return altdate.Tools.floatToString(value);
	}

	/**
		Convert Gregorian date in UTC
	*/
	public function toGregorianDate() : Gregorian {
		return JulianDay.convertToGregorianDate(this);
	}

	/**
		Convert Gregorian date/time in UTC
	*/
	public function toGregorianDateTime() : Gregorian {
		return JulianDay.convertToGregorianDateTime(this);
	}

	/**
		Round off value to nearest second
	*/
	public function round() : Void {
		value = convertToFloat(convertToTimeStruct(value));
	}

	/////////////////////////////////////////////////////////////////////
	//                     Static Methods                              //
	/////////////////////////////////////////////////////////////////////
	/**
		Return JulianDay number for UTC time
		Month is 0=January
	*/
	public static function calculate(year:Int, month:Int, day:Int, seconds:Int) : Float
	{
		var mLookup : Array<Int> =
			[0,-60,-30,0,31,61,92,122,153,184,214,245,275,306,337];
		// checked
		month = month + 1;
		var d : Float = day + Tools.secondsToDecimalDay(seconds);
		if(month < 3) {
			month = month + 12;
			year = year - 1;
		}
		var value = d
			//+ Tools.int((153 * month - 457) / 5)
			+ mLookup[month]
			+ 365 * year
			+ Math.floor(year / 4)
			- Math.floor(year / 100)
			+ Math.floor(year / 400)
			+ 1721118.5;

		return value;
	}

	/**
		Create a Julian Day from a Gregorian. Uses the values
		in Y/M/D H/M/S of the Gregorian, plus it's timezone
		to correct the Julian value.
	*/
	// Note: Code is very similar, and should be in sync with, tz.Zone
	// which is in /tzparser/statics directory. Don't modify the copy
	// in /tz/ !
	public static function fromGregorian(gdt : Gregorian) : JulianDay {
		var origAsTime = gdt.isDateTime();
		var jd : JulianDay = new JulianDay(origAsTime);
#if DEBUG_BUILD
		var seconds : Int = (gdt.hours * 3600) +
				(gdt.minutes * 60) +
				gdt.seconds;
		if(!origAsTime) {
			if(seconds != 0) {
				throw "Trying to convert Gregorian 'dateonly' with "+Std.string(seconds)+" seconds in it.";
			}
		}
#else true
		var seconds : Int = 0;
		if(origAsTime) {
		var seconds : Int = (gdt.hours * 3600) +
				(gdt.minutes * 60) +
				gdt.seconds;
		}
#end
		// get the julian value for the m/d/y h:m:s in the Gregorian date
		// this is a pretend UTC value for the time being
		var val : Float = altdate.JulianDay.calculate(gdt.year,gdt.month,gdt.day, seconds);

#if !PARSER_BUILD
		var record : tz.ChangeRecord = null;
		var _changes : Array<tz.ChangeRecord> = gdt.timezone.getChanges();
		var _abbrev : Array<String> = gdt.timezone.getAbbrev();

		for(i in _changes) {
			if(i.jdn < val)
				record = i;
			if(i.jdn > val)
				break;
		}
		if(record == null) {
			throw "Could not find a record to convert this date";
		}

		trace(record.utco +" "+ record.s);
		// adjust the value by the UTC offset plus the DST save value
		val = val + ((record.utco + record.s)/86400);
#end

		jd.value = val;
		return jd;
	}

	/**
		Convert Julian to Gregorian date in UTC
	*/
	public static function convertToGregorianDate(jdt : JulianDay) : Gregorian {
		var ts = convertToTimeStruct(jdt.value);
		var gd = new Gregorian();
#if !PARSER_BUILD
		gd.set(false,new tz.zones.etc.UTC(),ts.year, ts.month, ts.day);
#else true
		gd.set(false,null,ts.year, ts.month, ts.day);
#end
		return gd;
	}

	/**
		Convert Julian to Gregorian date/time in UTC
	*/
	public static function convertToGregorianDateTime(jdt : JulianDay) : Gregorian {
		var ts = convertToTimeStruct(jdt.value);
		var gd = new Gregorian();
#if !PARSER_BUILD
		gd.set(true,new tz.zones.etc.UTC(),ts.year,ts.month,ts.day,ts.hours,ts.minutes,ts.seconds);
#else true
		gd.set(true,null,ts.year,ts.month,ts.day,ts.hours,ts.minutes,ts.seconds);
#end
		return gd;
	}

	public static function convertToTimeStruct(jd:Float) : TimeStruct {
		// algo checked.
		var z : Int = Math.floor(jd - 1721118.5);
		var r : Float = jd - 1721118.5 - z;
		var g : Float = z - .25;
		var a : Int = Math.floor(g / 36524.25);
		var b : Int = a - Math.floor(a / 4);
		var year : Int = Math.floor((b+g) / 365.25);
		var c : Int = b + z - Math.floor(365.25 * year);
		var month : Int = Tools.int((5 * c + 456) / 153);
		var day : Float = c - Tools.int((153 * month - 457) / 5) + r;
		if(month > 12) {
			year = year + 1;
			month = month - 12;
		}
		var totalSeconds : Int = altdate.Tools.decimalDayTimeToSeconds(day);
		var hour : Int = Tools.int(totalSeconds / 3600);
		totalSeconds = totalSeconds - (hour * 3600);
		var min : Int = Math.floor(totalSeconds / 60);
		var sec : Int = totalSeconds - (min * 60);
		var dayInt : Int = Tools.int(day);
		var ts = {year:year,month:month-1,day:dayInt,hours:hour,minutes:min,seconds:sec};
		//trace(ts);
		return ts;
	}

	/**
		Take a Time Structure and convert to a JulianDay number
	*/
	public static function convertToFloat(ts:TimeStruct) : Float {
		var seconds : Int = (ts.hours * 3600) + (ts.minutes * 60) + ts.seconds;
		//trace(seconds);
		return calculate(ts.year,ts.month,ts.day, seconds);
	}
}
